package com.xianyun.rpc.utils;

import cn.hutool.core.io.resource.ResourceUtil;
import com.xianyun.rpc.serializer.Serializer;
import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 通过 spi 加载自定义序列化器
 * 使用 map 以 键名=>实现类 的方式保存
 * 维护一个 缓存 map，获取过一次的对象，不必重复创建
 * 编写获取实例的方法，根据键名从 map 中获取
 *
 * @author xianYun
 * @version 1.0
 **/
@Slf4j
public class SpiLoader {
    //键：类的全路径，值：配置文件中的键值 => 实现类的 Class 文件，（内层 map 就是配置文件的映射）
    private static final Map<String, Map<String, Class<?>>> loadMap = new ConcurrentHashMap<>();
    //缓存 map ，存放已加载类的实例。
    private static final Map<String, Object> cacheMap = new ConcurrentHashMap<>();
    private static final String RPC_SYSTEM_SPI_DIR = "META-INF/rpc/system/";
    private static final String RPC_CUSTOM_SPI_DIR = "META-INF/rpc/custom/";
    //需要扫描的目录
    private static final String[] SCAN_DIRS = new String[]{RPC_SYSTEM_SPI_DIR, RPC_CUSTOM_SPI_DIR};
    //需要动态加载的列表
    private static final List<Class<?>> LOAD_CLASS_LIST = Arrays.asList(Serializer.class);

    public static void loadAll() {
        for (Class<?> loadClass : LOAD_CLASS_LIST) {
            load(loadClass);
        }
    }

    /**
     * 获取 Clazz 对应的实现类。
     * 基本逻辑：从 loadMap 中获取对应的 Class 对象，从 Class 对象获取对应的对象实例。
     *
     * @param clazz class 对象
     * @param key   类的全路径
     * @param <T>
     * @return
     */
    public static <T> T getInstance(Class<T> clazz, String key) {
        //1. 参数校验。
        if (clazz == null || key == null) {
            throw new RuntimeException("参数不能为空");
        }
        //判断接口类型对应的配置文件映射是否存在
        Map<String, Class<?>> load = load(clazz);
        if (load == null) {
            throw new RuntimeException(String.format("SpiLoader 没有加载：", clazz.getName()));
        }
        //判断配置文件中是否存在此 key
        if (!load.containsKey(key)) {
            throw new RuntimeException(String.format("找不到对应 Class 对象%s", clazz.getName()));
        }

        //2. 判断缓存中是否有 Class 对象的实现类。有，则直接返回，没有则进行创建，放入缓存再返回。
        T instance = (T) cacheMap.get(clazz.getName());
        if (instance == null) {
            //加载此类型
            load(clazz);
            Class<?> aClass = loadMap.get(clazz.getName()).get(key);
            try {
                instance = (T) aClass.newInstance();
                cacheMap.put(clazz.getName(), instance);
            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        //3. 返回实例。
        return (T) cacheMap.get(clazz.getName());
    }

    /**
     * 根据传入的接口类型，将盘配置文件中对应的信息放进 loadMap 中。
     *
     * @param loadClass
     * @return
     */
    public static Map<String, Class<?>> load(Class loadClass) {
        log.info("开始加载 {} 类的实现类", loadClass.getName());
        Map<String, Class<?>> keyClassMap = new HashMap<>();
        for (String scanDir : SCAN_DIRS) {
            List<URL> resources = ResourceUtil.getResources(scanDir + loadClass.getName());
            for (URL resource : resources) {
                try {
                    InputStreamReader inputStreamReader = new InputStreamReader(resource.openStream());
                    BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                    String line;
                    while ((line = bufferedReader.readLine()) != null) {
                        String[] split = line.split("=");
                        String key = split[0];
                        String value = split[1];
                        keyClassMap.put(key, Class.forName(value));
                    }
                } catch (IOException e) {
                    log.error("SpiResource loadError:", e);
                    throw new RuntimeException(e);
                } catch (ClassNotFoundException e) {
                    log.error("SpiResource loadError:", e);
                    throw new RuntimeException(e);
                }
            }

        }
        loadMap.put(loadClass.getName(), keyClassMap);
        return keyClassMap;
    }
}


